﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Dynamic;

using X = Microsoft.Xna.Framework;

namespace XNA.BLEND.dom {
  internal class Document {
    internal Document() {
      this.FileBlocks = new Dictionary<UIntPtr, dynamic>();
    }

    internal void Load( string path ) {
      this.Reader = new BinaryReader( File.Open( path, FileMode.Open ) );

      this.LoadFileHeader();
      this.LoadFileBlocks();
    }

    internal void LoadFileBlocks() {
      dynamic block;
      do {
        block = this.LoadFileBlock1();
      } while ( block.__Code != "ENDB" );

      this.Reader.Close();

      foreach ( dynamic fb in this.FileBlocks.Values ) {
        this.LoadFileBlock2( fb );
      }
    }

    internal void LoadFileHeader() {
      dynamic fh = this.FileHeader = new ExpandoObject();

      string id = this.ReadBlendString( 7 );
      if ( id != "BLENDER" ) { throw new Exception( "Expected identification BLENDER." ); }
      
      fh.__BlendID = id;
      fh.__PointerSize = this.ReadPointerSize();
      fh.__Endianess = this.ReadEndianess();
      fh.__Version = this.ReadBlendString( 3 );
    }

    internal dynamic LoadFileBlock1() {
      dynamic r = new ExpandoObject();

      r.__Code = this.ReadBlendString( 4 );
      r.__StructSize = this.Reader.ReadInt32();
      r.__OldPointer = this.ReadBlendPointer();
      r.__StructType = this.Reader.ReadInt32();
      r.__StructCount = this.Reader.ReadInt32();

      // do instant load of SDNA-block &
      // delayed load of data in "normal" blocks
      if ( r.__Code == "DNA1" ) {
        this.LoadSDNABlock();
      }
      else {
        r.__RawData = this.Reader.ReadBytes( r.__StructSize );
        this.FileBlocks.Add( r.__OldPointer, r );

        if ( r.__Code == "SC" ) {
          this.SceneBlock = r;
        }
      }

      return r;
    }
    internal void LoadFileBlock2( dynamic fb ) {
      if ( fb.__Code != "DBA1" && fb.__RawData != null ) {
        this.Reader = new BinaryReader( new MemoryStream( fb.__RawData ) );

        if ( fb.__StructCount > 1 ) {
          List<dynamic> dbs = new List<dynamic>();

          for ( int i = 0; i < fb.__StructCount; i++ ) {
            dbs.Add( this.LoadDataBlock( fb ) );
          }

          ( (IDictionary<string, object>)( fb ) ).Add( SDNA.Types[SDNA.Structs[fb.__StructType].Type].ID, dbs );
        }
        else {
          dynamic db = this.LoadDataBlock( fb );
          ( (IDictionary<string, object>)( fb ) ).Add( SDNA.Types[SDNA.Structs[fb.__StructType].Type].ID, db );
        }
      }

      fb.__RawData = null;
    }

    internal void LoadSDNABlock() {
      this.SDNA = new ExpandoObject();
      this.SDNA.Document = this;

      string id = this.ReadBlendString( 4 );
      if ( id != "SDNA" ) { throw new Exception( "Expected identification SDNA." ); }

      // section NAME
      Dictionary<int, SDNAName> names = new Dictionary<int, SDNAName>();
      id = this.ReadBlendString( 4 );
      if ( id != "NAME" ) { throw new Exception( "Expected section NAME." ); }
      int count = this.Reader.ReadInt32();
      for ( int i = 0; i < count; i++ ) {
        names.Add( i, new SDNAName( this.ReadBlendString() ) ); 
      }
      this.AlignReader();
      this.SDNA.Names = names;

      // section TYPE
      Dictionary<int, SDNAType> types = new Dictionary<int, SDNAType>();
      id = this.ReadBlendString( 4 );
      if ( id != "TYPE" ) { throw new Exception( "Expected section TYPE." ); }
      count = this.Reader.ReadInt32();
      for ( int i = 0; i < count; i++ ) {
        types.Add( i, new SDNAType( this.ReadBlendString(), i ) );
      }
      this.SDNA.Types = types;
      this.AlignReader();

      // section TLEN
      id = this.ReadBlendString( 4 );
      if ( id != "TLEN" ) { throw new Exception( "Expected section TLEN." ); }
      for ( int i = 0; i < count; i++ ) {
        SDNAType type = this.SDNA.Types[i];
        type.Length = this.Reader.ReadInt16();
        this.SDNA.Types[i] = type;
      }
      this.AlignReader();

      // section STRC
      Dictionary<int, SDNAStruct> structs = new Dictionary<int, SDNAStruct>();
      Dictionary<int, SDNAStruct> typeStructs = new Dictionary<int, SDNAStruct>();
      id = this.ReadBlendString( 4 );
      if ( id != "STRC" ) { throw new Exception( "Expected section STRC." ); }
      count = this.Reader.ReadInt32();
      for ( int i = 0; i < count; i++ ) {
        SDNAStruct strc = new SDNAStruct( this.SDNA, this.Reader.ReadInt16() );
        int fieldcount = this.Reader.ReadInt16();
        for ( int j = 0; j < fieldcount; j++ ) {
          strc.AddField( j, new SDNAField( this.SDNA, this.Reader.ReadInt16(), this.Reader.ReadInt16() ) );
        }

        structs.Add( i, strc );
        typeStructs.Add( strc.Type, strc );
      }

      this.SDNA.Structs = structs;
      this.SDNA.TypeStructs = typeStructs;
    }

    internal dynamic LoadDataBlock(dynamic parent) {
      IDictionary<string, Object> db = (IDictionary<string, Object>)new ExpandoObject();
      SDNAStruct s = this.SDNA.Structs[parent.__StructType];

      if ( parent.__StructType != 0 ) {
        if ( parent.__StructSize >= s.TotalLength ) {
          foreach ( SDNAField field in s.Fields.Values ) {
            db.Add( SDNA.Names[field.Name].SimpleName, this.ReadValue( field ) );
          }
        }
        else if ( parent.__StructSize < s.TotalLength ) {
        }
        else {
        }
      }
      else {
        if ( parent.__StructSize == (int)( this.FileHeader.__PointerSize ) ) {
          db.Add("Pointer", this.ReadBlendPointer() );
        }
        else {
          db.Add( "Expression", this.ReadBlendString( parent.__StructSize ) );
        }
      }
      
      return db;
    }

    internal void SaveAsXML( string path ) {
      XmlDocument doc = this.Transform2Xml();

      doc.Save( path );
    }

    internal XmlDocument Transform2Xml() {
      XmlDocument doc = new XmlDocument();
      doc.AppendChild( doc.CreateXmlDeclaration( "1.0", "UTF-8", "yes" ) );
/*
      doc.AppendChild( this.FileHeader.Transform2Xml( doc, "BLENDER" ) );

      foreach ( FileBlock block in this.FileBlocks.Values ) {
        XmlElement fb = block.Transform2Xml( doc, "FileBlock" );
        doc.DocumentElement.AppendChild( fb );
      }
*/
      return doc;
    }

    internal object ReadValue( SDNAField field ) {
      object result = null;

      if ( field.IsArray ) {
        result = this.ReadArray( field, field.ArrayIndexes.Split( ',' ) );
      }
      else if ( field.IsStruct ) {
        result = this.ReadStruct( field );
      }
      else {
        result = this.ReadSimpleValue( field );
      }

      return result;
    }

    private object ReadArray( SDNAField field, string[] indexes ) {
      object result = null;

      int i0 = int.Parse( indexes[0] );

      if ( indexes.Length == 1 ) {
        if ( field.TypeName == "char" ) {
          result = this.ReadBlendString( int.Parse( indexes[0] ) );
        }
        else if ( field.TypeName == "float" && i0 == 3 ) {
          result = this.ReadVector3( field );
        }
        else if ( field.TypeName == "float" && i0 == 2 ) {
          result = this.ReadVector2( field );
        }
        else if ( field.TypeName == "short" && i0 == 3 ) {
          result = this.ReadVector3Short( field );
        }
        else if ( field.TypeName == "short" && i0 == 2 ) {
          result = this.ReadVector2Short( field );
        }
        else {
          result = this.Read1DimArray( field, int.Parse( indexes[0] ) );
        }
      }
      else if ( indexes.Length == 2 && field.TypeName == "float" ) {
        int i1 = int.Parse( indexes[1] );

        if ( i0 == i1 ) {
          if ( i0 == 3 ) {
            result = this.ReadMatrix3x3( field );
          }
          else if ( i0 == 4 ) {
            result = this.ReadMatrix4x4( field );
          }
          else {
            this.ReadMultiDimArray( field, indexes );
          }
        }
        else {
          this.ReadMultiDimArray( field, indexes );
        }
      }
      else {
        this.ReadMultiDimArray( field, indexes );
      }

      return result;
    }

    private object Read1DimArray( SDNAField field, int count ) {
      List<object> result = new List<object>();

      for ( int j = 0; j < count; j++ ) {
        if ( field.IsStruct ) {
          result.Add( this.ReadStruct( field ) );
        }
        else {
          result.Add( this.ReadSimpleValue( field ) );
        }
      }

      return result;
    }

    private object ReadMultiDimArray( SDNAField field, string[] indexes ) {
      object result = null;

      if ( indexes.Length == 1 ) {
        result = this.Read1DimArray( field, int.Parse( indexes[0] ) );
      }
      else {
        List<object> value = new List<object>();

        int count = int.Parse( indexes[0] );

        string[] recidx = new string[indexes.Length - 1];
        for ( int i = 1; i < indexes.Length; i++ ) {
          recidx[i - 1] = indexes[i];
        }

        for ( int i = 0; i < count; i++ ) {
          value.Add( this.ReadMultiDimArray( field, recidx ) );
        }

        result = value;
      }
      return result;
    }

    internal object ReadSimpleValue( SDNAField field ) {
      object result = null;

      if ( field.IsPointer ) {
        result = this.ReadBlendPointer();
      }
      else if ( field.TypeName == "char" ) {
        result = (char)this.Reader.ReadByte();
      }
      else if ( field.TypeName == "uchar" ) {
        result = (char)this.Reader.ReadByte();
      }
      else if ( field.TypeName == "short" ) {
        result = this.Reader.ReadInt16();
      }
      else if ( field.TypeName == "ushort" ) {
        result = this.Reader.ReadInt16();
      }
      else if ( field.TypeName == "int" ) {
        result = this.Reader.ReadInt32();
      }
      else if ( field.TypeName == "long" ) {
        result = this.Reader.ReadInt64();
      }
      else if ( field.TypeName == "ulong" ) {
        result = this.Reader.ReadInt64();
      }
      else if ( field.TypeName == "float" ) {
        result = this.Reader.ReadSingle();
      }
      else if ( field.TypeName == "double" ) {
        result = this.Reader.ReadDouble();
      }
      else if ( field.TypeName == "int64_t" ) {
        result = this.Reader.ReadInt64();
      }
      else if ( field.TypeName == "uint64_t" ) {
        result = this.Reader.ReadInt64();
      }
      else if ( field.TypeName == "void" ) {
        result = null;
      }
      else {
        throw new Exception( "Unexpected SimpleValue!" );
      }

      return result;
    }

    internal dynamic ReadStruct( SDNAField field ) {
      IDictionary<string, Object> result = (IDictionary<string, Object>)new ExpandoObject();

      SDNAStruct s = field.Struct;

      foreach ( SDNAField f in s.Fields.Values ) {
        dynamic child = this.ReadValue( f );

        result.Add( SDNA.Names[f.Name].SimpleName, child );
      }

      return result;
    }

    internal String ReadBlendString( int count ) {
      byte[] bytes = this.Reader.ReadBytes( count );

      return Document.Bytes2String( bytes );
    }
    internal String ReadBlendString() {
      String result = "";

      char c = this.Reader.ReadChar();
      while ( c != '\0' ) {
        result += c;

        c = this.Reader.ReadChar();
      }

      return result;
    }

    internal PointerSize ReadPointerSize() {
      PointerSize result = PointerSize.Unknown;

      char ps = this.Reader.ReadChar();

      if ( ps == '_' ) {
        result = PointerSize.Bits32;
      }
      else if ( ps == '-' ) {
        result = PointerSize.Bits64;
      }

      return result;
    }

    internal Endianess ReadEndianess() {
      Endianess result = Endianess.Unknown;

      char ps = this.Reader.ReadChar();

      if ( ps == 'v' ) {
        result = Endianess.Little;
      }
      else if ( ps == 'V' ) {
        result = Endianess.Big;
      }

      if ( result != Endianess.Little ) {
        throw new Exception( "Big Endian currently not supported." );
      }

      return result;
    }

    internal UIntPtr ReadBlendPointer() {
      UIntPtr result = UIntPtr.Zero;

      if ( this.FileHeader.__PointerSize == PointerSize.Bits32 ) {
        result = new UIntPtr(this.Reader.ReadUInt32() );
      }
      else if ( this.FileHeader.__PointerSize == PointerSize.Bits64 ) {
        uint ptr = (uint)this.Reader.ReadUInt64();
        result = new UIntPtr( ptr );
      }

      return result;
    }

    internal void AlignReader() {
      while ( this.Reader.PeekChar() == '\0' ) {
        this.Reader.ReadChar();
      }
    }

    private object ReadVector2( SDNAField field ) {
      X.Vector2 result = X.Vector2.Zero;

      result.X = this.Reader.ReadSingle();
      result.Y = this.Reader.ReadSingle();

      return result;
    }

    private object ReadVector3( SDNAField field ) {
      X.Vector3 result = X.Vector3.Zero;

      result.X = this.Reader.ReadSingle();
      result.Y = this.Reader.ReadSingle();
      result.Z = this.Reader.ReadSingle();

      return result;
    }

    private object ReadVector2Short( SDNAField field ) {
      X.Vector2 result = X.Vector2.Zero;

      result.X = this.Reader.ReadInt16();
      result.Y = this.Reader.ReadInt16();

      return result;
    }

    private object ReadVector3Short( SDNAField field ) {
      X.Vector3 result = X.Vector3.Zero;

      result.X = this.Reader.ReadInt16();
      result.Y = this.Reader.ReadInt16();
      result.Z = this.Reader.ReadInt16();

      return result;
    }

    private object ReadMatrix3x3( SDNAField field ) {
      X.Matrix result = X.Matrix.Identity;

      result.M11 = this.Reader.ReadSingle();
      result.M12 = this.Reader.ReadSingle();
      result.M13 = this.Reader.ReadSingle();

      result.M21 = this.Reader.ReadSingle();
      result.M22 = this.Reader.ReadSingle();
      result.M23 = this.Reader.ReadSingle();

      result.M31 = this.Reader.ReadSingle();
      result.M32 = this.Reader.ReadSingle();
      result.M33 = this.Reader.ReadSingle();

      return result;
    }

    private object ReadMatrix4x4( SDNAField field ) {
      X.Matrix result = X.Matrix.Identity;

      result.M11 = this.Reader.ReadSingle();
      result.M12 = this.Reader.ReadSingle();
      result.M13 = this.Reader.ReadSingle();
      result.M14 = this.Reader.ReadSingle();

      result.M21 = this.Reader.ReadSingle();
      result.M22 = this.Reader.ReadSingle();
      result.M23 = this.Reader.ReadSingle();
      result.M24 = this.Reader.ReadSingle();

      result.M31 = this.Reader.ReadSingle();
      result.M32 = this.Reader.ReadSingle();
      result.M33 = this.Reader.ReadSingle();
      result.M34 = this.Reader.ReadSingle();

      result.M41 = this.Reader.ReadSingle();
      result.M42 = this.Reader.ReadSingle();
      result.M43 = this.Reader.ReadSingle();
      result.M44 = this.Reader.ReadSingle();

      return result;
    }

    private static String Bytes2String( byte[] bytes ) {
      String result = System.Text.UTF8Encoding.UTF8.GetString( bytes );

      int pos = result.IndexOf( '\0' );
      if ( pos >= 0 ) {
        if ( pos > bytes.Length ) {
          pos = bytes.Length;
        }
        result = result.Substring( 0, pos );
      }

      return result;
    }

    internal dynamic FileHeader = null;
    internal Dictionary<UIntPtr, dynamic> FileBlocks = null;
    internal dynamic SceneBlock = null;
    internal dynamic SDNA = null;
  
    internal BinaryReader Reader = null;
  }

  internal enum PointerSize {
    Unknown = 0,
    Bits32 = 4,
    Bits64 = 8,
  }

  internal enum Endianess {
    Unknown,
    Little,
    Big,
  }
}
